docs(adr): identity trust-none default & trust pyramid#1264
docs(adr): identity trust-none default & trust pyramid#1264chaodu-agent wants to merge 1 commit into
Conversation
Split out from the per-platform-config ADR. Covers the security/trust decision independently: L1/L2/L3 trust pyramid, identity trust-none default at a single router gate, TrustConfig + carrier/type changes, echo-on-deny. Depends on the first-class per-platform config ADR.
Split the trust/security decision out into a separate ADR (docs/adr/identity-trust-none.md, PR #1264). This ADR now covers only the config schema change: first-class [platform] sections + [gateway] deprecation + migration.
|
LGTM ✅ — Well-structured ADR with clear layered trust model and thorough rejected alternatives. What This PR DoesIntroduces a new ADR documenting the identity trust-none default and 3-layer trust pyramid (L1 platform auth → L2 channel scope → L3 identity trust). Flips the default from allow-all to deny-all on the identity layer, with a single un-bypassable router gate. How It WorksDefines a Findings
What's Good (🟢)
Baseline Check
|
Implementation Plan — all platforms at once (phased)This plan implements the trust pyramid for every adapter behind one shared gate, then flips the identity default to deny-all as a separate, clearly-flagged breaking change. Invariant we're enforcing
Current state (verified)
What moves to the gate vs stays in adaptersGate (router) — L2 + L3 only:
Stays in adapters (structural/trigger — NOT trust):
Rationale: adapters answer "is this message a trigger for me?"; the gate answers "is this surface/identity trusted?" Bot admission + mention modes are platform-specific trigger semantics and are intentionally out of Phase 0 — shared infra (behavior-preserving)
Phase 1 — route every platform through the gate (still behavior-preserving)
Phase 2 — echo-on-deny UX
Phase 3 — flip the default (the breaking change, isolated PR)
Why phased
Edge cases / risks
Test matrix (per platform)human-allowed / human-denied / DM-from-stranger (echo) / channel-out-of-scope (no echo) / bot-sender (bot policy, not L3) / allow_all_users=true (legacy). Deliverables
Depends on #1263 (per-platform config) for the per-platform |
* feat(trust): Phase 0 — shared TrustConfig + PlatformTrustConfigs (additive) Defines the shared L2 (scope) + L3 (identity) trust types and pure decision function per the identity-trust-none ADR (#1264). Purely additive: not yet wired into AdapterRouter, changes no runtime behavior. - TrustConfig: L2 (allow_all_channels/allowed_channels/allow_dm, default open) + L3 (allow_all_users/allowed_users, default deny-all) - Decision enum (Allow / DenyScope / DenyIdentity) — only DenyIdentity echoes (request-access UX) - PlatformTrustConfigs registry keyed by platform() (no cross-platform ID bleed) - 11 unit tests covering the L2×L3×DM decision matrix Wiring + removing scattered per-adapter checks lands in Phase 1; the trust-none default flip lands in Phase 3. Refs #1264 * docs(trust): add compact decision-flow diagram to decide() rustdoc * fix(trust): address #1266 review (F2/F3/F6/F7 + F5 doc) - F2: #[non_exhaustive] on Decision (avoid future semver break) - F3: reword DenyScope doc — scope control, not an authorization failure - F6: normalize platform keys to lowercase (case-insensitive registry) - F7: empty sender_id is never identity-allowed (fail-closed, even under allow_all_users) - F5: document new() as canonical constructor; allow_all_* takes precedence - add 3 tests (empty-sender, case-insensitive registry) --------- Co-authored-by: chaodu-agent <chaodu-agent@users.noreply.github.com>
…ared gate (#1267) * feat(trust): Phase 1 (gateway) — route gateway ingress through shared trust gate Wires the shared L2/L3 gate (#1266) into the unified gateway path: - AdapterRouter gains a PlatformTrustConfigs registry (via with_trust builder — new()'s signature unchanged) + gate_incoming() ingress gate - process_gateway_event now enforces L2/L3 via router.gate_incoming(); should_skip_event keeps only bot-filter + @mention gating (its channel/user checks are neutered in the unified path) - registry built at startup from GATEWAY_* env, keyed per gateway platform Behavior-preserving: registry defaults mirror today's should_skip_event (allow-all default); is_dm passed false so DMs are evaluated as channels exactly as today. Discord/Slack routing + dispatch-path privatization + should_skip_event L2/L3 removal follow in later PRs. Deny-flip is Phase 3. Refs #1264 * docs(trust): add phase-2 TODO for is_dm carrier at gateway gate (review F2) --------- Co-authored-by: chaodu-agent <chaodu-agent@users.noreply.github.com>
…canary before merge] (#1270) * feat(trust): Phase 1 (discord) — gate L3 identity via shared gate Routes Discord ingress through AdapterRouter::gate_incoming for the L3 (identity) layer, keyed under "discord" in the trust registry. - registry "discord" entry: L2 open + allow_dm=true (Discord's richer channel/thread/DM logic stays in the adapter — the flat allowed_channels model can't express thread-by-parent admission); L3 mirrors resolved [discord].allow_all_users/allowed_users - gate call added at the Discord dispatch spawn, redundant-but-matching with Discord's existing pre-dispatch user check → non-regressive by construction (cannot deny what already passed) Behavior-preserving. Phase 1c makes the gate authoritative and removes the scattered user check; richer Discord L2 modeling + dispatch privatization tracked in #1269. Refs #1264 #1269 * fix(trust): Discord gate skips bots + passes real is_dm (review #1270 F1/F2) F1 (blocker): Discord's is_denied_user has a !is_bot bypass (bot admission is handled by allow_bot_messages + trusted_bot_ids). The shared L3 gate is human-identity only, so running it on bots wrongly dropped trusted bot-to-bot messages when allow_all_users=false (multi-agent). Guard the gate with !sender.is_bot. F2: pass the in-scope is_dm instead of hardcoded false (benign today with the L2-open discord entry, but avoids latent risk). Note: #1267 (gateway) is unaffected — should_skip_event's user check never had a bot bypass, so the gateway gate already matched it for bots. * test(trust): name + test the Discord L3 bot-bypass (review #1270 F4) Extract the gate's bot-skip into l3_gate_applies(is_bot) and add a regression test (l3_gate_skips_bots_admits_humans) locking in that bots bypass the shared L3 gate (mirrors is_denied_user's !is_bot), so trusted/mode-admitted bots aren't denied when allow_all_users=false. --------- Co-authored-by: chaodu-agent <chaodu-agent@users.noreply.github.com>
Splits the trust/security decision out of #1263 into its own ADR (per discussion — one decision per ADR).
Scope
allowed_usersfrom allow-all to deny-allAdapterRouter::handle_message()— un-bypassableMessageContext/ChannelRef, no new traitRelationship
allowed_userslive in the first-class sections.Doc:
docs/adr/identity-trust-none.md